package io.elegant.sessions

import android.content.Context
import android.media.SoundPool
import com.google.protobuf.Any
import io.elegant.*
import io.grpc.stub.StreamObserver
import kotlinx.coroutines.launch
import okhttp3.*
import java.io.File
import java.io.IOException

class SoundPoolSession(
    stub: ElegantApiGrpc.ElegantApiStub,
    private val context: Context
): BaseSession(stub, context) {
    override val sendStreamObserver: StreamObserver<Elegant.Interaction> = stub.soundPool(receiveStreamObserver)
    private var pool: SoundPool? = null
    private var okhttp: OkHttpClient? = null
    private val soundIds = mutableMapOf<String, Int>()
    private val callbacks = mutableMapOf<Int, () -> Unit>()
    override fun onCreate(sessionId: Long, futureId: Long, vararg argv: Any) {
        super.onCreate(sessionId, futureId, *argv)
        val poolSize = argv[0].toIntValue()
        okhttp = OkHttpClient()
            .newBuilder()
            .build()
        pool = SoundPool
            .Builder()
            .setMaxStreams(poolSize)
            .build()
        pool?.setOnLoadCompleteListener { _, i, _ ->
            callbacks[i]?.let {
                callbacks.remove(i)
                it()
            }
        }
    }
    override fun onDestroy() {
        soundIds.values.forEach {
            pool?.unload(it)
        }
        pool?.release()
        pool = null
        okhttp = null
        soundIds.clear()
        callbacks.clear()
        super.onDestroy()
    }
    override suspend fun onReceive(name: String, sessionId: Long, futureId: Long, vararg argv: Any) {
        when (name) {
            "load" -> load(sessionId, futureId, *argv)
            "play" -> play(sessionId, futureId, *argv)
            "stop" -> stop(*argv)
            else -> super.onReceive(name, sessionId, futureId, *argv)
        }
    }
    private fun load(sessionId: Long, futureId: Long, vararg argv: Any) {
        val args = argv.map { it.toStringValue() }
        val pathname = args.slice(1 until args.size)
        if (pathname.isEmpty()) {
            onLoad(sessionId, futureId, true.toAny())
            return
        }
        val baseUrl = args.first()
        val iter = pathname.iterator()
        fun next() {
            if (!iter.hasNext()) {
                onLoad(sessionId, futureId, true.toAny())
                return
            }
            val path = iter.next()
            val file = File(context.cacheDir, path)
            if (file.exists() && file.lastModified() + 86400000 > System.currentTimeMillis()) {
                next()
                return
            }
            file
                .parentFile
                ?.takeIf { !it.exists() }
                ?.mkdirs()
            val req = Request.Builder()
                .url(baseUrl + path)
                .build()
            okhttp?.newCall(req)?.enqueue(object: Callback {
                override fun onFailure(call: Call, e: IOException) = csMain.launch {
                    e.sendError(sessionId, futureId)
                }.let { }
                override fun onResponse(call: Call, response: Response) {
                    response
                        .body
                        .byteStream()
                        .use {
                            val buf = ByteArray(10240)
                            val output = file.outputStream()
                            var len = it.read(buf, 0, buf.size)
                            while (len > -1) {
                                output.write(buf, 0, len)
                                len = it.read(buf, 0, buf.size)
                            }
                            output.close()
                        }
                    csMain.launch {
                        next()
                    }
                }
            })
        }
        next()
    }
    private fun play(sessionId: Long, futureId: Long, vararg argv: Any) {
        val path = argv[0].toStringValue()
        val leftVolume = argv[1].toFloatValue()
        val rightVolume = argv[2].toFloatValue()
        val rate = argv[3].toFloatValue()
        val loop = argv[4].toIntValue()
        fun playInner() {
            val sid = soundIds[path]
            if (sid == null) {
                Exception("An error occurred in the sound pool, and the pa${path} sound is failed.").sendError(sessionId, futureId)
                return
            }
            val playId = pool?.play(sid, leftVolume, rightVolume, 0, loop, rate)
            onPlay(sessionId, futureId, playId.toAny())
        }
        if (soundIds.containsKey(path)) {
            playInner()
            return
        }
        val file = File(context.cacheDir, path)
        pool
            ?.load(file.path, 0)
            ?.let {
                soundIds[path] = it
                callbacks[it] = ::playInner
            }
    }
    private fun stop(vararg argv: Any) {
        val playId = argv[0].toIntValue()
        pool?.stop(playId)
    }
    private fun onLoad(sessionId: Long, futureId: Long, vararg argv: Any) = sendAction("onLoad", sessionId, futureId, *argv)
    private fun onPlay(sessionId: Long, futureId: Long, vararg argv: Any) = sendAction("onPlay", sessionId, futureId, *argv)
}